module(..., package.seeall)

require("cosmo")

function version_to_time(v, delimiter, tz)
   if not v then return nil end
   v = v .."000000000000000000000000" -- just in case we are a few digits short
   delimiter = delimiter or " "
   return v:sub(0,10)..delimiter..v:sub(12,13)..":"..v:sub(15,16)..":"..v:sub(18,19)..(tz or "")
end


--- Creates the HTML for the navigation bar.
--
--  @param page          A page table.
--  @param request        CGI parameters (not used).
--  @param sputnik       The Sputnik object.
--  @return              An HTML string.

function get_nav_bar (page, request, sputnik)

   cur_page = sputnik:dirify(page.name)          

   local nav_page, err = sputnik:load_config_page(
                 sputnik.config.DEFAULT_NAVIGATION_BAR)
   
   if err or (not nav_page.NAVIGATION) then
      return "Couldn't parse the navigation bar: " .. (error or "")
   end

   local nav = nav_page.NAVIGATION

   for i, section in ipairs(nav_page.NAVIGATION) do
      if not section.title then section.title = section.id end
      section.id = sputnik:dirify(section.id)
      if section.id == cur_page or section.id == page.category then
         section.is_active = true
         nav.current_section = section
      end

      for j, subsection in ipairs(section) do
         if not subsection.title then subsection.title = subsection.id end
         subsection.id = sputnik:dirify(subsection.id)
         if subsection.id == cur_page or subsection.id == page.category then
            section.is_active = true
            nav.current_section = section
            subsection.is_active = true
         end
      end
   end

   return cosmo.fill(page.templates.NAV_BAR,
      {  do_sections = function() 
            for i, section in ipairs(nav) do
               local class = "back"
               if section.is_active then 
                  class = "front"
               end
               cosmo.yield { 
                  class = class,
                  id    = section.id,
                  link   = sputnik:make_link(section.id),  
                  label = section.title}
            end
         end,
         do_subsections = function() 
            for i, subsection in ipairs(nav.current_section or {}) do
               local class = "back"
               if subsection.is_active then
                  class = "front"
               end
               cosmo.yield { 
                  class = class,
                  link = sputnik:make_link(subsection.id),
                  label = subsection.title 
               }
            end
         end,
      }
   )
end





local HtmlDiff = {}  -- a class do diff

--- Splits strings into tokens.
--
--  @param text          A string to be split.
--  @return              A list of tokens.

function HtmlDiff.diffsplit(self, text) 
   local l = {}
   text = text .. " "
   for token, sep in string.gmatch(text, "([^%s]*)(%s)") do
      l[#l+1] = token
      l[#l+1] = sep
   end
   return l
end

--- Returns a diff between two strings as HTML.
--
--  @param t1            One string.
--  @param t2            Another string.
--  @return              An HTML string.

function HtmlDiff.get_html_diff(self, t1, t2)

   local html = " <pre><code>"
   local t2 = self:diffsplit(t2)
   local t1 = self:diffsplit(t1)
      
   local l = {}
   require("lcs")
   
   local subseq = lcs.longestCommonSubseq(t1, t2, l)
   
   is = 1
   i1 = 0
   i2 = 1
   
   local state = ""
   
   function indiff(text)
      if #text > 0 then 
         return "<ins class='diffmod'>" .. text .. "</ins>"
      else
         return ""
      end
   end
   
   function outdiff(text) 
      if #text > 0 then
         return "<del class='diffmod'>" .. text .. "</del>"
      else 
         return ""
      end
   end

   while true do
      
      i1 = i1 + 1
      
      if i1 > #t1 then                                  -- done with t1 
         for j=i2,#t2 do
            html = html .. indiff(t2[j])
         end
         for j=is,#subseq do
            html = html .. subseq[j]
         end
         break                                          -- done
      end

      if t1[i1] == subseq[is] then 
         while true do   ---- fastforward through T2 ---
            if t2[i2] == t1[i1] or i2 > #t2 then 
               break 
            else
               if t2[i2] then 
                  html = html .. indiff(t2[i2]) 
               end
               i2 = i2 + 1
            end
         end
         
         --html = html .. outdiff(t1[i1])
         i2 = i2 + 1
         if subseq[is] then 
            html = html .. subseq[is]
         end
         is = is + 1
      else 
         html = html .. outdiff(t1[i1])
      end
   end
   
   html = html .. "</code></pre>"
   return html
end


-- ---------------------------------------------------------------------
--     Actions
-- ---------------------------------------------------------------------

actions = {}

--- Saves the new page submitted as a single string param, then
--- returns the HTML representation of the page.
--
--  @param request       request.params.raw_page is should contain the raw
--                       representation of the page.
--  @param sputnik       The sputnik table is used to save and reload 
--                       the page.

function actions.raw_save(page, request, sputnik)
   if not request.user then
      request.try_again = "YOU_MUST_BE_LOGGED_IN"
      return actions.raw_edit(page, request, sputnik)
   else
      sputnik:save_page(page, request.params.raw_page)
      page = sputnik:get_page(page.name)
      page = sputnik:decorate_page(page)
      return page.actions.show(page, request, sputnik)
   end
end


--- Saves the page submitted as a set of cgi params, then returns the
--- HTML representation of the page.
--
--  @param request       request.params fields are used to update the page.
--  @param sputnik       The sputnik table is used to save and reload 
--                       the page.  

function actions.save(page, request, sputnik)

   request.try_again = sputnik:check_form_token(request.params.form_token, request.params.form_timestamp)
   if not request.user then
      request.try_again = "YOU_MUST_BE_LOGGED_IN"
   elseif not page.check_permissions(request.user, "save") then
      request.try_again = "ACTION_NOT_ALLOWED"
   end
   if request.try_again then
      return actions.edit(page, request, sputnik)
   else
      local tmp_page = sputnik:update_page_with_params(page, request.params)
      new_page = sputnik:decorate_page(tmp_page)
      sputnik:save_page(new_page)
      return new_page.actions.show(new_page, request, sputnik)
   end
end



--- Returns just the content of the page, without the navigation bar,
--- the tool bars, etc.
--  
--  @param request       (Not used.)
--  @param sputnik       Used to wikify the page.

function actions.show_content(page, request, sputnik)
   return sputnik:wikify_text(page.content, page.templates)
end



--- Returns the complete HTML for the page.
--
--  @param request       (Not used.)
--  @param sputnik       

function actions.show(page, request, sputnik)
   request.is_indexable = true
   page.inner_html = page.actions.show_content(page, request, sputnik)
   return page.wrappers.default(page, request, sputnik)
end



--- Same as "show_content" but updates the page with values in
--- parameters before displaying it (used by both AHAH preview and
--- server-side preview).
--
--  @param request       request.params fields are used to update the page.
--  @param sputnik       The sputnik table is used to reload the page.

function actions.preview_content(page, request, sputnik)
   local tmp_page = sputnik:update_page_with_params(page, request.params)
   new_page = sputnik:decorate_page(tmp_page)
   return new_page.actions.show_content(new_page, request, sputnik)
end



--- Returns HTML showing a preview of the page (based on request.params)
--- and also a form to continue editing the page (the page is _not_
--- saved).
--
--  @param request       request.params fields are used to update the page.
--  @param sputnik       The sputnik table is used to save and reload 
--                       the page.  
--

function actions.preview(page, request, sputnik)
   request.preview = actions.preview_content(page, request, sputnik)
   return actions.edit(page, request, sputnik)
end


--- All "post" requests are routed through the same action ("post").
--- The reason for this is that we localize button labels, and their
--- values are not predictable for this reason.
--- Instead, we _name_ of the button to infer the action.
--- So, to request Page.save via post, we actually request
--- Page.post&action_save=foo, where foo could be anything.
--
--  @param request       We'll look for request.params.action_* to figure out 
--                       what we should be actually doing.
--  @return              HTML (whatever is returned by the action that
--                       it dispatches to).

function actions.post(page, request, sputnik)
   for k,v in pairs(request.params) do
      local action = string.match(k, "^action_(.*)$")
      if action then
         sputnik.logger:debug(action)
         return actions[action](page, request, sputnik) 
      end
   end
end

--- Returns history of changes for _all_ pages, by setting a
--- request.show_complete_history and then forwarding to
--- actions.history.

function actions.complete_history(page, request, sputnik)
   request.show_complete_history = 1
   return page.actions.history(page, request, sputnik)  
end


function actions.edits_by_recent_users(page, request, sputnik)
   request.show_complete_history = 1
   request.params.recent_users_only = 1
   return page.actions.history(page, request, sputnik)  
end

function mark_old_revisions(pages)
   local latest = {}
   local later
   for i, p in ipairs(pages) do
      if later then 
         later.previous = p.version
      end
      later = p
      if latest.name ~= p.name then
         latest = p
         latest.repeats = 0
         p.title_style = ""
      else
         latest.repeats = latest.repeats + 1
         p.repeats = 0
         p.stale = true
      end
   end
end


local calendar = require("sputnik.util.calendar")

--- Return HTML representing history of one page or all pages.
--
--  @param request       request.show_complete_history will determine 
--                       whether we show history for just this page 
--                       or for all pages.  request.params.date is an optional 
--                       filter

function actions.history(page, request, sputnik)

   local page_name = page.name
   local datelink                      
   if request.show_complete_history then
      page_name = nil
      datelink = function(date)
                    return sputnik:pseudo_page(sputnik.config.HISTORY_PAGE).links:show{date=date}
                 end
   else 
      datelink = function(date)
                    return page.links:history{date=date}
                 end
   end
   local title_filter = function(x) return x end

   local limit = 50
   if request.params.date and request.params.date:len() == 10 then 
      limit = 200
   end

   cal = calendar.Calendar:new()
   cal:select_date(request.params.date or calendar.today())

   local pages = sputnik:get_history(page_name, limit, request.params.date)

   mark_old_revisions(pages)

   tab = { 
      if_submit_button = cosmo.c(not request.show_complete_history){},
      do_revisions     = function()
                            for i, p in ipairs(pages) do
                               if (not request.params.recent_users_only) or sputnik.auth.is_recent(p.author) then
                                  local is_minor = (p.minor or ""):len() > 0
                                  cosmo.yield{
                                     version_link        = p.links:show{
                                                              version = p.version
                                                           },
                                     diff_link           = p.links:diff{
                                                              version=p.version, 
                                                              other=p.previous
                                                           },
                                     history_link        = p.links:history(),
                                     latest_link         = p.links:show(),
                                     version             = p.version,
                                     if_minor            = cosmo.c(is_minor){},
                                     title               = title_filter(p.name),
                                     author_link         = sputnik:make_link((p.author or "Anon")),
                                     author              = p.author,
                                     if_summary          = cosmo.c(p.summary and p.summary:len() > 0){
                                                              summary = p.summary
                                                           },
                                     if_specific_page    = cosmo.c(not request.show_complete_history){
                                                              if_minor = cosmo.c(is_minor){},
                                                           },
                                     if_complete_history = cosmo.c(request.show_complete_history){
                                                              if_stale = cosmo.c(p.stale){},
                                                              row_span = p.repeats + 1,
                                                              if_minor = cosmo.c(is_minor){},
                                                           }
                                  }
                               end
                            end
                         end, 
      current_month    = cal.mm,
      current_year     = cal.yyyy,
      do_dates         = function()
                            for d, current in ipairs(cal:get_days_of_the_month()) do
                               cosmo.yield{
                                  if_current_date = cosmo.c(current==1){
                                                       date=d
                                                    },
                                  if_other_date   = cosmo.c(current==0){
                                                       date=d,
                                                       date_link = datelink(cal:day_of_this_month_to_string(d))
                                                    }
                               }
                            end
                          end,
       do_months       = function()
                            for m, current in ipairs(cal:get_months_of_the_year()) do
                               local month_name = calendar.MONTHS[m][1]
                               cosmo.yield{
                                  if_current_month = cosmo.c(current==1){
                                                        month = month_name,
                                                     },
                                  if_other_month   = cosmo.c(current==0){
                                                        month = month_name,
                                                        month_link = datelink(cal:month_of_this_year_to_string(m))
                                                     }
                               }
                            end
                         end,
      version          = page.version,
      base_url         = sputnik.config.BASE_URL,
      page_name        = page.name,
   }

   page.inner_html = cosmo.fill(page.templates.HISTORY, tab)
   return page.wrappers.default(page, request, sputnik)
end



--- Same as "show_complete_history" but returns RSS instead of HTML.
--
--

function actions.complete_history_rss(page, request, sputnik)
   request.show_complete_history = 1
   return actions.rss(page, request, sputnik)  
end

function actions.rss_for_edits_by_recent_users(page, request, sputnik)
   request.show_complete_history = 1
   request.params.recent_users_only = 1
   return actions.rss(page, request, sputnik)  
end



--- Same as "history" but returns RSS instead of HTML.
--
--

function actions.rss(page, request, sputnik)
   local page_name = page.name
   local title = "Recent Edits to '" .. page.title .."'"
   if request.show_complete_history then
      page_name = nil
      title = "Recent Wiki Edits"
   end
   local history = sputnik:get_history(page_name, 50)
   sputnik.logger:debug("recent_users_only="..(request.params.recent_users_only or "nil")) 
   return cosmo.fill(page.templates.RSS, {  
      title   = title,
      baseurl = sputnik.config.BASE_URL, 
      items   = function()
                   for i, page in ipairs(history) do
                      if (not request.params.recent_users_only) or sputnik.auth.is_recent(page.author) then
                         sputnik.logger:debug("recent_users_only="..(request.params.recent_users_only or "nil")) 
                         sputnik.logger:debug("user recent?"..tostring(sputnik.auth.is_recent(page.author)))
                         cosmo.yield{
                            link        = "http://" .. sputnik.config.DOMAIN ..
                                          sputnik:escape_url(
                                             page.urls:show{version=page.version}
                                          ),
                            title       = string.format("%s: %s by %s",
                                                        page.version,
                                                        page.title or "",
                                                        page.author or ""),
                            ispermalink = "false",
                            guid        = page.name .. "/" .. page.version,
                            summary     = page.summary
                         }
                      end
                   end
                end,
   }), "application/rss+xml"
end




function actions.list_pages(page, request, sputnik)
   local page_names = sputnik:get_page_names()
   local function yield_page(name) 
      cosmo.yield{name=name, url=sputnik.config.NICE_URL..name}
   end
   local special_prefix = "^[_@]"
   page.inner_html = cosmo.f(page.templates.LIST_OF_ALL_PAGES){
                        do_regular_pages = function()
                                              for i, name in ipairs(page_names) do
                                                 if not string.match(name, special_prefix) then
                                                    yield_page(name)
                                                 end
                                              end
                                           end,   
                        do_special_pages = function()
                                              for i, name in ipairs(page_names) do
                                                 if string.match(name, special_prefix) then
                                                    yield_page(name)
                                                 end
                                              end
                                           end,
                     }
   return page.wrappers.default(page, request, sputnik)
end

function actions.show_sitemap_xml(page, request, sputnik)
   local page_names = sputnik:get_page_names()
   return cosmo.f(page.templates.SITEMAP_XML){
      do_urls = function()
          for i, name in ipairs(page_names) do
             if string.match(name, "^[%a%d]") then
                local priority, url
                
                if name == sputnik.config.HOME_PAGE then
                   url = sputnik.config.HOME_PAGE_URL
                   priority = ".9"
                else
                   url = sputnik.config.NICE_URL..name
                   priority = ".1"
                end
                cosmo.yield{
                   url = "http://"..sputnik.config.DOMAIN..url,
                   lastmod = version_to_time(sputnik:get_page(name).version, "T", sputnik.config.SERVER_TZ),
                   changefreq = "weekly",
                   priority = priority
                }
             end
          end
      end,   
   }, "text/xml"
end

--- Displays HTML for a login form.
--
--

function actions.login_form(page, request, sputnik)
   page.inner_html = cosmo.fill(page.templates.LOGIN, 
                                {  action_url = sputnik.config.BASE_URL,
                                   page_name  = sputnik:dirify(page.name) } )
   return page.wrappers.default(page, request, sputnik)
end




--- Shows HTML for the standard Edit field.
--
--

function actions.edit (page, request, sputnik)

   -- select the parameters that should be copied
   local fields = {}
   for field, field_params in pairs(page.fields) do
      if not field_params.virtual then
         fields[field] = sputnik:escape(request.params[field] or page._vnode[field])
      end
   end
   fields.page_name = sputnik:dirify(page.name)  -- page name cannot be changed
   fields.user=""
   fields.password=""
   fields.minor=nil
   fields.summary=""

   login_spec = [[
         insert{4.0, "note", "please_login_or_create_an_account"}
         insert{4.1, "text_field",    "user"}
         insert{4.2, "password",      "password"}      
   ]]

   if request.user then
      login_spec = ""
   end 
   
   local honeypots = "" 
   math.randomseed(os.time())
   for i=0, (sputnik.config.NUM_HONEYPOTS_IN_FORMS or 0) do
      local field_name = "honey"..tostring(i)
      honeypots = honeypots.."\n"..cosmo.f([[   insert{$order, "honeypot", "$name"}]]){
                                order = tostring(math.random()*5),
                                name  = field_name,
                             }
      fields[field_name] = ""
   end

   local html_forms = require("sputnik.util.html_forms")
   
   local form_token, form_timestamp = sputnik:get_current_form_token()
   
   local html_for_fields, field_list = html_forms.make_html_form{
                                          field_spec = page.edit_ui..login_spec..honeypots, 
                                          templates  = page.templates, 
                                          translator = sputnik.translator,
                                          values     = fields,
                                          hash_fn    = function(field_name)
                                                          return sputnik:hash_field_name(field_name, form_token)
                                                       end
                                       }
   
   page.inner_html = cosmo.f(page.templates.EDIT){
                        if_try_again    = cosmo.c(request.try_again){
                                             alert = sputnik.translator.translate_key(request.try_again or "")
                                          },
                        if_preview      = cosmo.c(request.preview){
                                             preview = request.preview,
                                             summary = fields.summary
                                          },
                        html_for_fields = html_for_fields,
                        page_name       = page.name,
                        post_fields     = table.concat(field_list,","),
                        form_token      = form_token,
                        form_timestamp  = form_timestamp,
                        action_url      = sputnik.config.BASE_URL,
                     }
   return page.wrappers.default(page, request, sputnik)
end

--- Shows HTML of diff between two versions of the page.
--
--

function actions.diff(page, request, sputnik)
   local other_page = sputnik:get_page(page.name, request.params.other) 
   page.inner_html  = cosmo.f(page.templates.DIFF){  
                         version1 = request.params.version,
                         link1    = page.links:show{version=request.params.version},
                         author1  = page.author,
                         version2 = request.params.other,
                         link2    = page.links:show{version=request.params.other},
                         author2  = other_page.author,
                         diff     = HtmlDiff:get_html_diff(other_page.content, page.content) 
                      }
   return page.wrappers.default(page, request, sputnik)
end

--- Shows the raw content of the page with content-type set to
--- text/plain (note that unlike actions.raw, this method only returns
--- the _content_ of the page, not its metadata).

function actions.raw_content(page, request, sputnik)
   return page.content, "text/plain"
end



--- Shows the underlying string representation of the page as plain text.
--- 
--

function actions.raw(page, request, sputnik)
   return page.raw or "No source available.", "text/plain"
end


--- Shows the _content of the page shown as 'code'.
--
--

function actions.show_content_as_code(page, request, sputnik)
   local escaped = sputnik:escape(page.content) 
   return "<pre><code>"..escaped.."</code></pre>"
end


--- Shows the content of the page as Lua code, checking whether it
--  parses.

function actions.show_content_as_lua_code(page, request, sputnik)

   local DOLLAR_REPLACEMENT = "$<span></span>"
   local escaped = sputnik:escape(page.content)
   escaped = escaped:gsub("%$", DOLLAR_REPLACEMENT)
   escaped = escaped:gsub(" ", "&nbsp;")
   escaped = string.gsub(escaped, "(%-%-[^\n]*)", 
                         function (comment) return "<font color='gray'>"..comment.."</font>" end)
   local f, errors = loadstring(page.content)
   if errors then
      local reg_exp = "^.+%]%:(%d+)%:"
      error_line_num = string.match(errors, reg_exp)
      errors = string.gsub(errors, reg_exp, "On line %1:")
   end

   return cosmo.f(page.templates.LUA_CODE){
             do_lines  = function()
                            local i = 0
                            for line in ("\n"..escaped):gmatch"\n([^\n]*)" do
                               i = i+1
                               local class = "ok"
                               if i == tonumber(error_line_num) then
                                  class = "bad"
                               end
                               cosmo.yield{
                                  i = i, 
                                  line = line,
                                  class=class
                               }
                            end
                         end,
             if_ok     = cosmo.c(f~=nil){},
             if_errors = cosmo.c(errors~=nil){errors=errors},
          }
end


--- Shows the HTML of the whole page, with the content of the page
--  shown as 'code'.

function actions.code(page, request, sputnik)
   page.inner_html = page.actions.show_content_as_code(page, request, sputnik)
   return page.wrappers.default(page, request, sputnik)
end



--- Shows the HTML for an error message when a non-existent action is
--  requested.

function actions.action_not_found(page, request, sputnik)
   page.inner_html = cosmo.f(page.templates.ACTION_NOT_FOUND){
                        title             = page.title,
                        url               = page.urls:show(),
                        action            = request.action,
                        if_custom_actions = cosmo.c(page._vnode.actions and page._vnode.actions:len() > 0){
                                               actions = page._vnode.actions
                                            }
                     }
   return page.wrappers.default(page, request, sputnik)
end

function actions.sputnik_version(page, request, sputnik)
   page.inner_html = sputnik.config.VERSION or "<no version information>"
   return page.wrappers.default(page, request, sputnik)
end
   

wrappers = {}

--- Wraps the HTML content in bells and whistles such as the
--- navigation bar, the header, the footer, etc.

function wrappers.default(page, request, sputnik) 

   local is_old = request.params.version and (page.version ~= page.history.current)

   return cosmo.f(page.templates.MAIN){  
      site_title       = sputnik.config.SITE_TITLE or "",
      title            = page.title,
      do_stylesheets   = function()
                            for i, url in ipairs(sputnik.config.STYLESHEETS) do
                               cosmo.yield {url=url}
                            end
                         end,
      nav_bar          = get_nav_bar(page, request, sputnik),
      if_no_index      = cosmo.c((not request.is_indexable) or is_old){},
      if_old_version   = cosmo.c(is_old){
                            version      = version_to_time(request.params.version)
                         },
      if_logged_in     = cosmo.c(request.user){  
                            user         = request.user,
                            logout_link  = page.links:show{logout="1"} 
                         },
      if_search        = cosmo.c(sputnik.config.SEARCH_PAGE){
                            base_url    = sputnik.config.BASE_URL,
                            search_page = sputnik.config.SEARCH_PAGE,
                            search_box_content = sputnik.config.SEARCH_CONTENT or "", 
                         },
      content          = page.inner_html,
      sidebar          = "",
      
      -- "links" include "href="
      page_link        = sputnik:make_link(page.name),
      edit_link        = page.links:edit{version = request.params.version},
      history_link     = page.links:history(),
      site_rss_link    = sputnik:pseudo_page(sputnik.config.HISTORY_PAGE).links:rss(),
      page_rss_link    = page.links:rss(),
      -- urls are just urls
      logo_url         = sputnik.config.LOGO_URL,
      home_page_url    = sputnik.config.HOME_PAGE_URL,
      icons_url        = sputnik.config.ICONS_URL,
      sputnik_url      = "href='http://sputnik.freewisdom.org/'"
   }, "text/html"
end

